﻿using System;
using System.Collections.Generic;
using System.Threading;
using System.Net;
using System.Net.Sockets;

using AnB.Common;

namespace AnB.Net
{
    public class Connection : ConnectionBase
    {
        public Connection(Socket socket, Dispatcher dispatcher) : base(socket, HEAD_LEN + Packet.HEAD_LEN)
        {
            mDispatcher = dispatcher;
        }

        private Dispatcher mDispatcher;
        private CyclicCounter mIdCounter = new CyclicCounter(1, ushort.MaxValue);

        public const long DEFAULT_TIMEOUT = 10 * 1000; // 10 seconds

        public class RequestOptions
        {
            public long Timeout;
        }

        private class RequestWait
        {
            public long TimeoutId;
            public Action<object> Success;
            public Action<Error.Code, object> Error;
        }

        private class PacketRecord
        {
            public Packet Packet;
            public CmdHandler CmdHandler;
        }

        protected override long HandleHead(byte[] head, int index, int count, out Object state)
        {
            Packet packet = new Packet(mDispatcher);
            packet.DeserializeHead(head, index, count);
            CmdHandler cmdHandler;
            try
            {
                cmdHandler = mDispatcher.GetCmdHandler(packet.Cmd);
            }
            catch (Exception e)
            {
                Log.Get().ErrorFormat("{0} 0X{1:X4} get cmd handler error {2}\n{3}", Alias, packet.Cmd, e.Message, e.StackTrace);
                state = null;
                return -1;
            }
            state = new PacketRecord()
            {
                Packet = packet,
                CmdHandler = cmdHandler,
            };
            return cmdHandler.SizeLimit;
        }

        protected override void HandleBody(byte[] body, int index, int count, object state)
        {
            var packetRecord = (PacketRecord)state;
            Packet packet = packetRecord.Packet;
            CmdHandler cmdHandler = packetRecord.CmdHandler;

            try
            {
                packet.DeserialzeBody(body, index, count);
            }
            catch (Exception e)
            {
                Log.Get().ErrorFormat("{0} parse Packet error {1}\n{2}", Alias, e.Message, e.StackTrace);
                Stop();
                return;
            }

            string requestDescription = string.Format("{0} {1}/id:{2}", Alias, cmdHandler.Description, packet.Id);

            if (packet.Response)
            {
                ushort id = packet.Id;

                RequestWait requestWait;
                bool hasRequestWait;
                lock (Requests)
                {
                    hasRequestWait = Requests.TryGetValue(id, out requestWait);
                    if (hasRequestWait)
                    {
                        Requests.Remove(id);
                    }
                }
                if (!hasRequestWait)
                {
                    Log.Get().ErrorFormat("{0} RequestWait not found", requestDescription);
                    return;
                }

                AnB.Common.Timeout.Instance.Remove(requestWait.TimeoutId);
                if (packet.ErrorCode == Error.Code.None)
                {
                    Log.Get().ErrorFormat("{0} response", requestDescription);
                    requestWait.Success(packet.Data);
                }
                else
                {
                    Log.Get().ErrorFormat("{0} response {1} {2}", requestDescription, packet.ErrorCode, packet.Data);
                    requestWait.Error(packet.ErrorCode, packet.Data);
                }
                return;
            }

            if (cmdHandler == null)
            {
                return;
            }

            Log.Get().InfoFormat("{0} enters", requestDescription);
            object ret;
            Error.Code error;
            Packet.BodyType bodyType;
            try
            {
                ret = cmdHandler.Handle(this, packet.Data);
                error = Error.Code.None;
                bodyType = cmdHandler.OutBody;
            }
            catch (Exception e)
            {
                ret = string.Format("{0}\n{1}", e.Message, e.StackTrace);
                error = Error.Code.RuntimeException;
                bodyType = Packet.BodyType.String;
                Log.Get().ErrorFormat("{0} {1} {2}", requestDescription, error, ret);
            }
            Log.Get().InfoFormat("{0} exits", requestDescription);
            if (!packet.NeedResponse)
            {
                Log.Get().DebugFormat("{0} NeedResponse=false", requestDescription);
                return;
            }
            var retPacket = new Packet(mDispatcher)
            {
                Id = packet.Id,
                Cmd = packet.Cmd,
                Type = bodyType,
                Response = true,
                NeedResponse = false,
                Compress = false,
                ErrorCode = error,
                Data = ret,
            };
            var segments = retPacket.Serialize();
            SendLengthy(segments);
        }

        private Dictionary<ushort, RequestWait> Requests = new Dictionary<ushort, RequestWait>();

        private void AppendRequest(CmdHandler cmdHandler, object data, Action<object> success, Action<Error.Code, object> error, RequestOptions requestOptions)
        {
            ushort id = (ushort)mIdCounter.Next;
            string requestDescription = string.Format("{0} {1}/id:{2}", Alias, cmdHandler.Description, id);
            Log.Get().InfoFormat("{0} request", requestDescription);

            bool needResponse = success != null || error != null;
            var packet = new Packet()
            {
                Id = id,
                Cmd = cmdHandler.Cmd,
                Type = cmdHandler.InBody,
                Response = false,
                NeedResponse = needResponse,
                Data = data,
            };
            if (needResponse)
            {
                long timeout = (requestOptions != null && requestOptions.Timeout >= 0) ? requestOptions.Timeout : DEFAULT_TIMEOUT;
                long timeoutId = AnB.Common.Timeout.Instance.Add(timeout, () =>
                    {
                        lock (Requests)
                        {
                            if (!Requests.Remove(id))
                            {
                                return;
                            }
                        }
                        string err = string.Format("{0} request timeout", requestDescription, id);
                        Log.Get().ErrorFormat(err);
                        if (error != null)
                        {
                            error(Error.Code.RequestTimeout, err);
                        }
                    });
                var requestWait = new RequestWait()
                    {
                        TimeoutId = timeoutId,
                        Success = success,
                        Error = error,
                    };
                lock (Requests)
                {
                    Requests.Add(id, requestWait);
                }
            }
            var segments = packet.Serialize();
            SendLengthy(segments);
        }

        public void RequestAsync<T>(string cmd, object data, Action<T> success = null, Action<Error.Code, object> error = null, RequestOptions requestOptions = null)
        {
            Action<object> success1 = success != null ? obj =>
                    {
                        success((T)obj);
                    } : (Action<object>)null;
            AppendRequest(mDispatcher.GetCmdHandler(cmd), data, success1, error, requestOptions);
        }
        public T Request<T>(string cmd, object data, RequestOptions requestOptions = null)
        {
            Error.Code error0 = Error.Code.None;
            object ret0 = null;
            ManualResetEvent manualResetEvent = new ManualResetEvent(false);
            RequestAsync<T>(cmd, data, (ret) =>
                {
                    ret0 = ret;
                    manualResetEvent.Set();
                },
                (error, ret) =>
                {
                    error0 = error;
                    ret0 = ret;
                    manualResetEvent.Set();
                }, requestOptions);
            manualResetEvent.WaitOne();
            if (error0 != Error.Code.None)
            {
                throw new Error(error0, ret0);
            }
            return (T)ret0;
        }
    }
}
